//
// jDataView by Vjeux - Jan 2010
//
// A unique way to read a binary file in the browser
// http://github.com/vjeux/jDataView
// http://blog.vjeux.com/ <vjeuxx@gmail.com>
//

(function (global) {

    var compatibility = {
        ArrayBuffer: typeof ArrayBuffer !== 'undefined',
        DataView: typeof DataView !== 'undefined' &&
            ('getFloat64' in DataView.prototype || // Chrome
                'getFloat64' in new DataView(new ArrayBuffer(1))), // Node
        // NodeJS Buffer in v0.5.5 and newer
        NodeBuffer: typeof Buffer !== 'undefined' && 'readInt16LE' in Buffer.prototype
    };

    var dataTypes = {
        'Int8': 1,
        'Int16': 2,
        'Int32': 4,
        'Uint8': 1,
        'Uint16': 2,
        'Uint32': 4,
        'Float32': 4,
        'Float64': 8
    };

    var nodeNaming = {
        'Int8': 'Int8',
        'Int16': 'Int16',
        'Int32': 'Int32',
        'Uint8': 'UInt8',
        'Uint16': 'UInt16',
        'Uint32': 'UInt32',
        'Float32': 'Float',
        'Float64': 'Double'
    };

    var jDataView = function (buffer, byteOffset, byteLength, littleEndian) {
        if (!(this instanceof jDataView)) {
            throw new Error("jDataView constructor may not be called as a function");
        }

        this.buffer = buffer;

        // Handle Type Errors
        if (!(compatibility.NodeBuffer && buffer instanceof Buffer) &&
            !(compatibility.ArrayBuffer && buffer instanceof ArrayBuffer) &&
            typeof buffer !== 'string') {
            throw new TypeError('jDataView buffer has an incompatible type');
        }

        // Check parameters and existing functionnalities
        this._isArrayBuffer = compatibility.ArrayBuffer && buffer instanceof ArrayBuffer;
        this._isDataView = compatibility.DataView && this._isArrayBuffer;
        this._isNodeBuffer = compatibility.NodeBuffer && buffer instanceof Buffer;

        // Default Values
        this._littleEndian = littleEndian === undefined ? false : littleEndian;

        var bufferLength = this._isArrayBuffer ? buffer.byteLength : buffer.length;
        if (byteOffset === undefined) {
            byteOffset = 0;
        }
        this.byteOffset = byteOffset;

        if (byteLength === undefined) {
            byteLength = bufferLength - byteOffset;
        }
        this.byteLength = byteLength;

        if (!this._isDataView) {
            // Do additional checks to simulate DataView
            if (typeof byteOffset !== 'number') {
                throw new TypeError('jDataView byteOffset is not a number');
            }
            if (typeof byteLength !== 'number') {
                throw new TypeError('jDataView byteLength is not a number');
            }
            if (byteOffset < 0) {
                throw new Error('jDataView byteOffset is negative');
            }
            if (byteLength < 0) {
                throw new Error('jDataView byteLength is negative');
            }
        }

        // Instanciate
        if (this._isDataView) {
            this._view = new DataView(buffer, byteOffset, byteLength);
            this._start = 0;
        }
        this._start = byteOffset;
        if (byteOffset + byteLength > bufferLength) {
            throw new Error("jDataView (byteOffset + byteLength) value is out of bounds");
        }

        this._offset = 0;

        // Create uniform reading methods (wrappers) for the following data types

        if (this._isDataView) { // DataView: we use the direct method
            for (var type in dataTypes) {
                if (!dataTypes.hasOwnProperty(type)) {
                    continue;
                }
                (function (type, view) {
                    var size = dataTypes[type];
                    view['get' + type] = function (byteOffset, littleEndian) {
                        // Handle the lack of endianness
                        if (littleEndian === undefined) {
                            littleEndian = view._littleEndian;
                        }

                        // Handle the lack of byteOffset
                        if (byteOffset === undefined) {
                            byteOffset = view._offset;
                        }

                        // Move the internal offset forward
                        view._offset = byteOffset + size;

                        return view._view['get' + type](byteOffset, littleEndian);
                    }
                })(type, this);
            }
        } else if (this._isNodeBuffer && compatibility.NodeBuffer) {
            for (var type in dataTypes) {
                if (!dataTypes.hasOwnProperty(type)) {
                    continue;
                }

                var name;
                if (type === 'Int8' || type === 'Uint8') {
                    name = 'read' + nodeNaming[type];
                } else if (littleEndian) {
                    name = 'read' + nodeNaming[type] + 'LE';
                } else {
                    name = 'read' + nodeNaming[type] + 'BE';
                }

                (function (type, view, name) {
                    var size = dataTypes[type];
                    view['get' + type] = function (byteOffset, littleEndian) {
                        // Handle the lack of endianness
                        if (littleEndian === undefined) {
                            littleEndian = view._littleEndian;
                        }

                        // Handle the lack of byteOffset
                        if (byteOffset === undefined) {
                            byteOffset = view._offset;
                        }

                        // Move the internal offset forward
                        view._offset = byteOffset + size;

                        return view.buffer[name](view._start + byteOffset);
                    }
                })(type, this, name);
            }
        } else {
            for (var type in dataTypes) {
                if (!dataTypes.hasOwnProperty(type)) {
                    continue;
                }
                (function (type, view) {
                    var size = dataTypes[type];
                    view['get' + type] = function (byteOffset, littleEndian) {
                        // Handle the lack of endianness
                        if (littleEndian === undefined) {
                            littleEndian = view._littleEndian;
                        }

                        // Handle the lack of byteOffset
                        if (byteOffset === undefined) {
                            byteOffset = view._offset;
                        }

                        // Move the internal offset forward
                        view._offset = byteOffset + size;

                        if (view._isArrayBuffer && (view._start + byteOffset) % size === 0 && (size === 1 || littleEndian)) {
                            // ArrayBuffer: we use a typed array of size 1 if the alignment is good
                            // ArrayBuffer does not support endianess flag (for size > 1)
                            return new global[type + 'Array'](view.buffer, view._start + byteOffset, 1)[0];
                        } else {
                            // Error checking:
                            if (typeof byteOffset !== 'number') {
                                throw new TypeError('jDataView byteOffset is not a number');
                            }
                            if (byteOffset + size > view.byteLength) {
                                throw new Error('jDataView (byteOffset + size) value is out of bounds');
                            }

                            return view['_get' + type](view._start + byteOffset, littleEndian);
                        }
                    }
                })(type, this);
            }
        }
    };

    if (compatibility.NodeBuffer) {
        jDataView.createBuffer = function () {
            var buffer = new Buffer(arguments.length);
            for (var i = 0; i < arguments.length; ++i) {
                buffer[i] = arguments[i];
            }
            return buffer;
        }
    } else if (compatibility.ArrayBuffer) {
        jDataView.createBuffer = function () {
            var buffer = new ArrayBuffer(arguments.length);
            var view = new Int8Array(buffer);
            for (var i = 0; i < arguments.length; ++i) {
                view[i] = arguments[i];
            }
            return buffer;
        }
    } else {
        jDataView.createBuffer = function () {
            return String.fromCharCode.apply(null, arguments);
        }
    }

    jDataView.prototype = {
        compatibility: compatibility,

        // Helpers

        getString: function (length, byteOffset) {
            var value;

            // Handle the lack of byteOffset
            if (byteOffset === undefined) {
                byteOffset = this._offset;
            }

            // Error Checking
            if (typeof byteOffset !== 'number') {
                throw new TypeError('jDataView byteOffset is not a number');
            }
            if (length < 0 || byteOffset + length > this.byteLength) {
                throw new Error('jDataView length or (byteOffset+length) value is out of bounds');
            }

            if (this._isNodeBuffer) {
                value = this.buffer.toString('ascii', this._start + byteOffset, this._start + byteOffset + length);
            } else {
                value = '';
                for (var i = 0; i < length; ++i) {
                    var char = this.getUint8(byteOffset + i);
                    value += String.fromCharCode(char > 127 ? 65533 : char);
                }
            }

            this._offset = byteOffset + length;
            return value;
        },

        getChar: function (byteOffset) {
            return this.getString(1, byteOffset);
        },

        tell: function () {
            return this._offset;
        },

        seek: function (byteOffset) {
            if (typeof byteOffset !== 'number') {
                throw new TypeError('jDataView byteOffset is not a number');
            }
            if (byteOffset < 0 || byteOffset > this.byteLength) {
                throw new Error('jDataView byteOffset value is out of bounds');
            }

            return this._offset = byteOffset;
        },

        // Compatibility functions on a String Buffer

        _endianness: function (byteOffset, pos, max, littleEndian) {
            return byteOffset + (littleEndian ? max - pos - 1 : pos);
        },

        _getFloat64: function (byteOffset, littleEndian) {
            var b0 = this._getUint8(this._endianness(byteOffset, 0, 8, littleEndian)),
                b1 = this._getUint8(this._endianness(byteOffset, 1, 8, littleEndian)),
                b2 = this._getUint8(this._endianness(byteOffset, 2, 8, littleEndian)),
                b3 = this._getUint8(this._endianness(byteOffset, 3, 8, littleEndian)),
                b4 = this._getUint8(this._endianness(byteOffset, 4, 8, littleEndian)),
                b5 = this._getUint8(this._endianness(byteOffset, 5, 8, littleEndian)),
                b6 = this._getUint8(this._endianness(byteOffset, 6, 8, littleEndian)),
                b7 = this._getUint8(this._endianness(byteOffset, 7, 8, littleEndian)),

                sign = 1 - (2 * (b0 >> 7)),
                exponent = ((((b0 << 1) & 0xff) << 3) | (b1 >> 4)) - (Math.pow(2, 10) - 1),

                // Binary operators such as | and << operate on 32 bit values, using + and Math.pow(2) instead
                mantissa = ((b1 & 0x0f) * Math.pow(2, 48)) + (b2 * Math.pow(2, 40)) + (b3 * Math.pow(2, 32)) +
                (b4 * Math.pow(2, 24)) + (b5 * Math.pow(2, 16)) + (b6 * Math.pow(2, 8)) + b7;

            if (exponent === 1024) {
                if (mantissa !== 0) {
                    return NaN;
                } else {
                    return sign * Infinity;
                }
            }

            if (exponent === -1023) { // Denormalized
                return sign * mantissa * Math.pow(2, -1022 - 52);
            }

            return sign * (1 + mantissa * Math.pow(2, -52)) * Math.pow(2, exponent);
        },

        _getFloat32: function (byteOffset, littleEndian) {
            var b0 = this._getUint8(this._endianness(byteOffset, 0, 4, littleEndian)),
                b1 = this._getUint8(this._endianness(byteOffset, 1, 4, littleEndian)),
                b2 = this._getUint8(this._endianness(byteOffset, 2, 4, littleEndian)),
                b3 = this._getUint8(this._endianness(byteOffset, 3, 4, littleEndian)),

                sign = 1 - (2 * (b0 >> 7)),
                exponent = (((b0 << 1) & 0xff) | (b1 >> 7)) - 127,
                mantissa = ((b1 & 0x7f) << 16) | (b2 << 8) | b3;

            if (exponent === 128) {
                if (mantissa !== 0) {
                    return NaN;
                } else {
                    return sign * Infinity;
                }
            }

            if (exponent === -127) { // Denormalized
                return sign * mantissa * Math.pow(2, -126 - 23);
            }

            return sign * (1 + mantissa * Math.pow(2, -23)) * Math.pow(2, exponent);
        },

        _getInt32: function (byteOffset, littleEndian) {
            var b = this._getUint32(byteOffset, littleEndian);
            return b > Math.pow(2, 31) - 1 ? b - Math.pow(2, 32) : b;
        },

        _getUint32: function (byteOffset, littleEndian) {
            var b3 = this._getUint8(this._endianness(byteOffset, 0, 4, littleEndian)),
                b2 = this._getUint8(this._endianness(byteOffset, 1, 4, littleEndian)),
                b1 = this._getUint8(this._endianness(byteOffset, 2, 4, littleEndian)),
                b0 = this._getUint8(this._endianness(byteOffset, 3, 4, littleEndian));

            return (b3 * Math.pow(2, 24)) + (b2 << 16) + (b1 << 8) + b0;
        },

        _getInt16: function (byteOffset, littleEndian) {
            var b = this._getUint16(byteOffset, littleEndian);
            return b > Math.pow(2, 15) - 1 ? b - Math.pow(2, 16) : b;
        },

        _getUint16: function (byteOffset, littleEndian) {
            var b1 = this._getUint8(this._endianness(byteOffset, 0, 2, littleEndian)),
                b0 = this._getUint8(this._endianness(byteOffset, 1, 2, littleEndian));

            return (b1 << 8) + b0;
        },

        _getInt8: function (byteOffset) {
            var b = this._getUint8(byteOffset);
            return b > Math.pow(2, 7) - 1 ? b - Math.pow(2, 8) : b;
        },

        _getUint8: function (byteOffset) {
            if (this._isArrayBuffer) {
                return new Uint8Array(this.buffer, byteOffset, 1)[0];
            } else if (this._isNodeBuffer) {
                return this.buffer[byteOffset];
            } else {
                return this.buffer.charCodeAt(byteOffset) & 0xff;
            }
        }
    };

    if (typeof jQuery !== 'undefined' && jQuery.fn.jquery >= "1.6.2") {
        var convertResponseBodyToText = function (byteArray) {
            // http://jsperf.com/vbscript-binary-download/6
            var scrambledStr;
            try {
                scrambledStr = IEBinaryToArray_ByteStr(byteArray);
            } catch (e) {
                // http://stackoverflow.com/questions/1919972/how-do-i-access-xhr-responsebody-for-binary-data-from-javascript-in-ie
                // http://miskun.com/javascript/internet-explorer-and-binary-files-data-access/
                var IEBinaryToArray_ByteStr_Script =
                    "Function IEBinaryToArray_ByteStr(Binary)\r\n" +
                    "	IEBinaryToArray_ByteStr = CStr(Binary)\r\n" +
                    "End Function\r\n" +
                    "Function IEBinaryToArray_ByteStr_Last(Binary)\r\n" +
                    "	Dim lastIndex\r\n" +
                    "	lastIndex = LenB(Binary)\r\n" +
                    "	if lastIndex mod 2 Then\r\n" +
                    "		IEBinaryToArray_ByteStr_Last = AscB( MidB( Binary, lastIndex, 1 ) )\r\n" +
                    "	Else\r\n" +
                    "		IEBinaryToArray_ByteStr_Last = -1\r\n" +
                    "	End If\r\n" +
                    "End Function\r\n";

                // http://msdn.microsoft.com/en-us/library/ms536420(v=vs.85).aspx
                // proprietary IE function
                window.execScript(IEBinaryToArray_ByteStr_Script, 'vbscript');

                scrambledStr = IEBinaryToArray_ByteStr(byteArray);
            }

            var lastChr = IEBinaryToArray_ByteStr_Last(byteArray),
                result = "",
                i = 0,
                l = scrambledStr.length % 8,
                thischar;
            while (i < l) {
                thischar = scrambledStr.charCodeAt(i++);
                result += String.fromCharCode(thischar & 0xff, thischar >> 8);
            }
            l = scrambledStr.length
            while (i < l) {
                result += String.fromCharCode(
                    (thischar = scrambledStr.charCodeAt(i++), thischar & 0xff), thischar >> 8,
                    (thischar = scrambledStr.charCodeAt(i++), thischar & 0xff), thischar >> 8,
                    (thischar = scrambledStr.charCodeAt(i++), thischar & 0xff), thischar >> 8,
                    (thischar = scrambledStr.charCodeAt(i++), thischar & 0xff), thischar >> 8,
                    (thischar = scrambledStr.charCodeAt(i++), thischar & 0xff), thischar >> 8,
                    (thischar = scrambledStr.charCodeAt(i++), thischar & 0xff), thischar >> 8,
                    (thischar = scrambledStr.charCodeAt(i++), thischar & 0xff), thischar >> 8,
                    (thischar = scrambledStr.charCodeAt(i++), thischar & 0xff), thischar >> 8);
            }
            if (lastChr > -1) {
                result += String.fromCharCode(lastChr);
            }
            return result;
        };

        jQuery.ajaxSetup({
            converters: {
                '* dataview': function (data) {
                    return new jDataView(data);
                }
            },
            accepts: {
                dataview: "text/plain; charset=x-user-defined"
            },
            responseHandler: {
                dataview: function (responses, options, xhr) {
                    // Array Buffer Firefox
                    if ('mozResponseArrayBuffer' in xhr) {
                        responses.text = xhr.mozResponseArrayBuffer;
                    }
                    // Array Buffer Chrome
                    else if ('responseType' in xhr && xhr.responseType === 'arraybuffer' && xhr.response) {
                        responses.text = xhr.response;
                    }
                    // Internet Explorer (Byte array accessible through VBScript -- convert to text)
                    else if ('responseBody' in xhr) {
                        responses.text = convertResponseBodyToText(xhr.responseBody);
                    }
                    // Older Browsers
                    else {
                        responses.text = xhr.responseText;
                    }
                }
            }
        });

        jQuery.ajaxPrefilter('dataview', function (options, originalOptions, jqXHR) {
            // trying to set the responseType on IE 6 causes an error
            if (jQuery.support.ajaxResponseType) {
                if (!options.hasOwnProperty('xhrFields')) {
                    options.xhrFields = {};
                }
                options.xhrFields.responseType = 'arraybuffer';
            }
            options.mimeType = 'text/plain; charset=x-user-defined';
        });
    }

    global.jDataView = (global.module || {}).exports = jDataView;
    if (typeof module !== 'undefined') {
        module.exports = jDataView;
    }

})(this);
